package com.extremelylargepackage.entity;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.util.Log;

import com.extremelylargepackage.GameResources;
import com.extremelylargepackage.entity.Enemy;
import com.extremelylargepackage.entity.Entity;
import com.extremelylargepackage.level.LevelEvent;
import com.extremelylargepackage.sprite.AbstractSprite;
import com.extremelylargepackage.sprite.Animation;
import com.extremelylargepackage.sprite.AnimationParser;

public class Dialog extends Entity implements LevelEvent {
	public static final int TOP = 1;
	public static final int BOTTOM = 2;

	/**
	 * Specifies how far from the top/bottom of the screen the dialog
	 * box should be displayed (given the TOP or BOTTOM positioning).
	 */
	private static final int DEFAULT_Y_OFFSET = 10;

	/**
	 * Specifies the inner x padding for each side of the dialog box.
	 */
	private static final int PADDING_X = 10;

	/**
	 * Number of lines to display.
	 */
	private static final int DISPLAYED_LINES = 3;

	/**
	 * Number of characters to be displayed per second.
	 */
	private static final double TEXT_SPEED = 20;

	/**
	 * Time in milliseconds to wait between "pages".
	 */
	private static final int PAGE_WAIT = 2000;

	/**
	 * A singleton list containing only this Dialog. Used for passing to Level.
	 */
	private final List<Entity> _scenery = Collections.singletonList((Entity) this);

	/**
	 * Queue containing pages that are to be displayed.
	 */
	private Queue<Page> _text;

	/**
	 * Text that is currently displayed on screen, separated by lines.
	 */
	private String[] _displayedLines = new String[DISPLAYED_LINES];
	private int _currentLine;

	/**
	 * Character in current line that is next to be displayed.
	 */
	private int _currentChar;

	/**
	 * Time in milliseconds since the last character was displayed.
	 */
	private int _elapsed;

	/**
	 * Specifies whether we are waiting at the end of the current page or not.
	 */
	private boolean _endOfPage = false;

	private Paint _textPaint;

	private Animation _currentAnim;
	private String _currentName;

	/**
	 * Whether or not to continue the level script while showing the dialog.
	 */
	private boolean _fallThrough;

	private Dialog()
	{
		_sprite = GameResources.getResource("speech_box_nc");
		_locX = (GameResources.SCREEN_WIDTH - _sprite.getWidth()) / 2;
		setPosition(TOP);

		_name = "dialog";
		_status = Status.scenery;

		_text = new LinkedList<Page>();

		_textPaint = new Paint();
		_textPaint.setColor(Color.WHITE);
		_textPaint.setTypeface(Typeface.MONOSPACE);
		_textPaint.setStyle(Paint.Style.STROKE);
		_textPaint.setTextSize(GameResources.getFontSize());

		for(int i = 0; i < _displayedLines.length; ++i)
			_displayedLines[i] = "";
	}

	public static Dialog parse(Element dialogElem)
	{
		Dialog dialog = new Dialog();

		dialog._fallThrough = dialogElem.getAttribute("fallthrough").equals("true");

		NodeList nodes = dialogElem.getChildNodes();
		for(int i = 0; i < nodes.getLength(); ++i) {
			Node n = nodes.item(i);
			if(n.getNodeType() == Node.TEXT_NODE) {
				Text textNode = (Text) n;
				String text = textNode.getData().trim().replace("\t", "");
				dialog._text.addAll(dialog.divideIntoPages(text));
			} else if(n.getNodeType() == Node.ELEMENT_NODE) {
				Element elem = (Element)n;
				if(n.getNodeName().equals("icon")) {
					if(!elem.getAttribute("sprite").equals("")) {
						// Load static sprite and wrap in Animation object
						AbstractSprite sprite = GameResources.getResource(elem.getAttribute("sprite"));
						dialog._currentAnim = new Animation("/sprite=" + elem.getAttribute("sprite"));
						dialog._currentAnim.addFrame(sprite, 1);
					} else if(!elem.getAttribute("animation").equals("")) {
						try {
							dialog._currentAnim = AnimationParser.parseAnimation(elem.getAttribute("animation"));
						} catch (Exception e) {
							Log.e("Dialog parse error", "Unable to parse animation '" +
									elem.getAttribute("animation") + "'", e);
						}
					} else {
						dialog._currentAnim = null;
					}
				}
			}
		}

		Page firstPage = dialog._text.peek();
		dialog._currentAnim = firstPage._icon;
		dialog._currentName = firstPage._speakerName;

		return dialog;
	}

	private LinkedList<Page> divideIntoPages(String text)
	{
		LinkedList<Page> res = new LinkedList<Page>();

		Page page = new Page();
		page._icon = _currentAnim;
		page._speakerName = _currentName;

		while(text.length() > 0) {
			// Determine how much of the string can be displayed on screen.
			int cutoff = _textPaint.breakText(text, true, getAvailableWidth(), null);

			// Unless the rest of the text fits on one line, break the current line off at the last word.
			if(cutoff < text.length()) {
				// Look for where the last word is broken off. We'll cut the line off at this point.
				int lastWordPos = text.lastIndexOf(' ', cutoff + 1); // + 1 in case there's whitespace after
				lastWordPos = Math.max(lastWordPos, text.lastIndexOf('\n', cutoff + 1)); // match ' ' or '\n'
				if(lastWordPos > 0)
					cutoff = lastWordPos + 1; // + 1 so that the space is included in this line rather than the next.
			}

			// A newline in the level definition indicates a new page.
			int lineBreak = text.indexOf('\n');
			boolean lineBreakFound = lineBreak > 0 && lineBreak < cutoff;
			if(lineBreakFound)
				cutoff = lineBreak;

			// Add the line that was just read to the current page.
			page._pageLines[page._lines++] = text.substring(0, cutoff);
			if(page._lines >= DISPLAYED_LINES || lineBreakFound) {
				// If the page is full or the user forced a new page, add it to the list and continue to the next one.
				res.add(page);

				page = new Page();
				page._icon = _currentAnim;
				page._speakerName = _currentName;
			}

			// Remove the line that was just read, and then remove all leading newlines
			text = text.substring(cutoff).replaceAll("^\n+", "");
		}

		// Add the final page, if it contains anything.
		if(page._lines > 0)
			res.add(page);

		return res;
	}

	public void setTextColor(int color)
	{
		_textPaint.setColor(color);
	}

	/**
	 * Position the text box using one of the predefined constants.
	 * 
	 * @param position
	 */
	public void setPosition(int position)
	{
		switch(position) {
			case BOTTOM:
				_locY = GameResources.SCREEN_HEIGHT - _sprite.getHeight() - DEFAULT_Y_OFFSET;
				 break;
			case TOP:
				_locY = DEFAULT_Y_OFFSET;
				break;
			default:
				throw new IllegalArgumentException("Position type \"" + position + "\" not recognized.");
		}
	}

	@Override
	public void updateState(long time)
	{
		_elapsed += time;
		if(_currentAnim != null)
			_currentAnim.update(time);

		if(!_endOfPage) {
			while(_elapsed >= (1000/TEXT_SPEED)) { // 1000 / TEXT_SPEED == time in ms per character
				displayCharacter();

				_elapsed -= 1000 / TEXT_SPEED;
			}
		} else {
			if(_elapsed >= PAGE_WAIT) {
				_text.poll();
				if(!_text.isEmpty()) {
					_currentLine = 0;
					for(int i = 0; i < _displayedLines.length; ++i)
						_displayedLines[i] = "";

					Page currentPage = _text.peek();
					_currentAnim = currentPage._icon;
					_currentName = currentPage._speakerName;

					_elapsed -= PAGE_WAIT;
					_endOfPage = false;
				} else {
					_status = Status.dead;
				}
			}
		}
	}

	private void displayCharacter()
	{
		Page page = _text.peek();
		String line = page._pageLines[_currentLine];
		_displayedLines[_currentLine] += line.charAt(_currentChar);
		++_currentChar;

		if(_currentChar >= line.length()) {
			++_currentLine;
			_currentChar = 0;

			if(_currentLine >= page._lines) {
				_currentLine = page._lines - 1; // only display lines we have
				_endOfPage = true;
			}
		}
	}

	@Override
	public void onCollision(Entity z)
	{
		// really?
	}

	@Override
	public void draw(Canvas c)
	{
		if(!_text.isEmpty()) {
			c.drawBitmap(_sprite.getBitmap(), _locX, _locY, null);

			float textX;
			if(_currentAnim == null) {
				textX = _locX + PADDING_X;
			} else {
				float iconY = _locY + (_sprite.getHeight() - _currentAnim.getHeight())/2;
				_currentAnim.draw(c, _locX + PADDING_X, iconY);
				textX = _locX + (PADDING_X*2) + _currentAnim.getWidth();
			}

			float textHeight = _textPaint.getFontSpacing() * _text.peek()._lines;
			float textY = _locY + (_sprite.getHeight() - textHeight)/2 +
					_textPaint.getTextSize() - _textPaint.descent()/2;
			for(int i = 0; i <= _currentLine; ++i) {
				c.drawText(_displayedLines[i], textX, textY, _textPaint);
				textY += _textPaint.getFontSpacing();
			}
		}
	}

	private float getAvailableWidth()
	{
		if(_currentAnim == null)
			return _sprite.getWidth() - (PADDING_X * 2);
		else
			return _sprite.getWidth() - (PADDING_X * 3) - _currentAnim.getWidth();
	}

	@Override
	public Map<Integer, Enemy> getEnemies()
	{
		return Collections.emptyMap();
	}

	@Override
	public boolean finished()
	{
		return _fallThrough || _text.isEmpty();
	}

	@Override
	public Collection<Entity> getScenery()
	{
		return _scenery;
	}

	@Override
	public Map<String, Object> getState()
	{
		Map<String, Object> res = super.getState();
	
		List<Map<String, Object>> textState = new ArrayList<Map<String, Object>>();
		for(Page page : _text) {
			textState.add(page.getState());
		}
		res.put("text", textState);
	
		res.put("displayedLines", Arrays.asList(_displayedLines));
		res.put("currentLine", String.valueOf(_currentLine));
		res.put("currentChar", String.valueOf(_currentChar));
		res.put("elapsed", String.valueOf(_elapsed));
		res.put("endOfPage", String.valueOf(_endOfPage));
		res.put("textColor", String.valueOf(_textPaint.getColor()));
		res.put("fallThrough", String.valueOf(_fallThrough));
	
		return res;
	}

	private class Page
	{
		private String[] _pageLines = new String[DISPLAYED_LINES];
		private int _lines;
		private Animation _icon;
		private String _speakerName;

		public Map<String, Object> getState()
		{
			Map<String, Object> res = new HashMap<String, Object>();

			res.put("lines", String.valueOf(_lines));
			res.put("icon", _icon.getName());
			res.put("speakerName", _speakerName);
			res.put("pageLines", Arrays.asList(_pageLines));

			return res;
		}
	}
}
